--[[ -- Created by tdef -- Modified by Tronex -- Added: 2018/12/15 -- Last edit: 2019/7/6 -- Pull undercover agent out from the enemy squad Parameters for precondition P[1] = (string) task_id P[2] = (string) task giver faction, if "nil" then it will get it from task giver object P[3] = (bool) if true, defined factions in "status_functor_params" are enemies. otherwise, it will generate enemy faction list based on defined factions P[4] = (bool) if true, includes scripted squads P[5] = (num) scan mode (1 to 5) 1 = same level 2 = same or nearby levels 3 = neaby levels only 4 = far levels 5 = all levels task_loadout_spy: the weapon carried by the spy, that's how you identify him. You can set as many weapons sections here (seperated by ,) and the script will pick one task_loadout_other: the weapon carried by other members, that's how you distinguish them from the spy. You can set as many weapons sections here (seperated by ,) and the script will pick one per member task_msg_on_join: the message spy send when he's freed from his squad task_msg_on_leave: the message spy send after you escort him to the task giver Example of usage in configs: precondition = validate_spy_task( mil_smart_terrain_7_7_freedom_leader_stalker_task_2 : freedom : true : false : 3 ) target_functor = spy_task_target_functor status_functor = spy_task_status_functor status_functor_params = dolg task_loadout_spy = wpn_svd task_loadout_other = wpn_ak74 task_msg_on_join = mil_smart_terrain_7_7_freedom_leader_stalker_task_2_msg_join task_msg_on_leave = mil_smart_terrain_7_7_freedom_leader_stalker_task_2_msg_leave on_job_descr = %=setup_spy_task( mil_smart_terrain_7_7_freedom_leader_stalker_task_2 )% --]] local faction_list = { -- List of factions targeted in agent rescue tasks "stalker", "dolg", "freedom", "csky", "ecolog", "killer", "army", "bandit", --"monolith", } local blacklisted_maps = { -- List of maps to skip in scans -- North ["l13_generators"] = true, ["l12_stancia_2"] = true, ["l12_stancia"] = true, ["l11_pripyat"] = true, ["l10_radar"] = true, ["l11_hospital"] = true, -- Underground ["jupiter_underground"] = true, ["labx8"] = true, ["l03u_agr_underground"] = true, ["l04u_labx18"] = true, ["l08u_brainlab"] = true, ["l10u_bunker"] = true, ["l12u_control_monolith"] = true, ["l12u_sarcofag"] = true, ["l13u_warlab"] = true, -- Other ["l05_bar"] = true, ["fake_start"] = true, } local cache = {} local enable_debug = false local sfind = string.find function printl(...) if enable_debug then printf(...) end end ---------------------------< Utility >--------------------------- function generate_squad_loadout(task_id) local sq_loadout = {} local sq_loadout_table = parse_list(task_manager.task_ini,task_id,"task_loadout_other") if (#sq_loadout_table == 0) then sq_loadout = {"wpn_ak74","ammo_5.45x39_fmj"} else local wpn = sq_loadout_table[math.random(#sq_loadout_table)] local ammo = parse_list(ini_sys,wpn,"ammo_class") or {} if ini_sys:section_exist(wpn) and (#ammo > 0) then sq_loadout = { wpn , ammo[math.random(#ammo)] } else sq_loadout = {"wpn_ak74","ammo_5.45x39_fmj"} end end return sq_loadout end function join_message(task_id, id) local se_obj = alife_object(id) if se_obj then local str = task_manager.task_ini:r_string_ex(task_id, "task_msg_on_join") or 'spy_task_join' local name, icon = game.translate_string(se_obj:character_name()), se_obj:character_icon() dynamic_news_helper.send_tip(game.translate_string(str), name,nil,nil, icon, nil, 'npc') return end printf('! join_message: se_obj (%s) is nil!', id) end function leave_message(task_id, id) local se_obj = alife_object(id) if se_obj then local str = task_manager.task_ini:r_string_ex(task_id, "task_msg_on_leave") or 'spy_task_leave' local name, icon = game.translate_string(se_obj:character_name()), se_obj:character_icon() dynamic_news_helper.send_tip(game.translate_string(str), name,nil,nil, icon, nil, 'npc') return end printf('! leave_message: se_obj (%s) is nil!', id) end function postpone_for_next_frame(task_id, squad_id) local squad = alife_object(squad_id) if (squad) then -- Location local location = alife():level_name(game_graph():vertex(squad.m_game_vertex_id):level_id()) for k in squad:squad_members() do local se_obj = k.object or alife_object(k.id) if se_obj then location = dynamic_news_helper.GetPointDescription(se_obj) break end end local str_location = game.translate_string("st_location") .. " " .. location -- Community local str_comm = "" local community = squad.player_id if is_squad_monster[community] then str_comm = game.translate_string("st_sq_type") .. " " .. game.translate_string(community) else str_comm = game.translate_string("st_mm_new_game_faction_2") .. " " .. game.translate_string(community) end -- Build News local news_caption = game.translate_string(task_manager.task_ini:r_string_ex(task_id, "title")) or "error" local news_text = str_comm .. "\\n " .. str_location local news_ico = news_manager.tips_icons[squad.player_id] or task_manager.task_ini:r_string_ex(task_id, "icon") or "ui_iconsTotal_mutant" --local news_ico = task_manager.task_ini:r_string_ex(task_id, "icon") or "ui_iconsTotal_mutant" --local news_text = str_location db.actor:give_talk_message2(news_caption, news_text, news_ico, "iconed_answer_item") end return true end ---------------------------< Target functor >--------------------------- local stage_tgt_f = {} stage_tgt_f[1] = function(task, task_id) local var = load_var(db.actor, task_id) if var then return var.squad_id end return nil end stage_tgt_f[2] = function(tsk, task_id) return tsk.task_giver_id end stage_tgt_f[3] = function(tsk, task_id) return tsk.task_giver_id end task_functor.spy_task_target_functor = function(task_id,field,p,tsk) if not (db.actor and tsk) then return nil end local stage = tsk.stage if stage_tgt_f[stage] then return stage_tgt_f[stage](tsk,task_id) end return nil end ---------------------------< Status functor >--------------------------- local stage_f = {} stage_f[0] = function(task, task_id) -- setup stage local var = load_var(db.actor, task_id) if (not var) then printf('var nil in task %s', task_id) return "fail" end local squad_id = var.squad_id if (not squad_id) then printf('squad_id nil in task %s', task_id) return "fail" end local squad = alife_object(squad_id) if (not squad) then printf('se_squad nil in task %s', task_id) return "fail" end local members = {} for k in squad:squad_members() do members[k.id] = true printl('~ %s | squad member: %s', task_id, k.id) end local spy_id = random_key_table(members) printl('~ %s | spy: %s', task_id, spy_id) -- Spy loadout local spy_loadout = {} local spy_loadout_table = parse_list(task_manager.task_ini,task_id,"task_loadout_spy") if (#spy_loadout_table == 0) then spy_loadout = {"wpn_svd","ammo_7.62x54_ap"} else local wpn = spy_loadout_table[math.random(#spy_loadout_table)] local ammo = parse_list(ini_sys,wpn,"ammo_class") or {} if ini_sys:section_exist(wpn) and (#ammo > 0) then spy_loadout = { wpn , ammo[math.random(#ammo)] } else spy_loadout = {"wpn_svd","ammo_7.62x54_ap"} end end local sim = alife() for i=1,65534 do local se_obj = sim:object(i) if se_obj then if se_obj.parent_id and (se_obj.parent_id == spy_id) and sfind(se_obj:section_name(),"wpn_") then -- release all guns from spy printl('- %s | releasing weapon %s from spy %s', task_id, se_obj:section_name(), se_obj.parent_id) alife_release(se_obj) elseif se_obj.parent_id and (not (se_obj.parent_id == spy_id)) and members[se_obj.parent_id] and sfind(se_obj:section_name(),spy_loadout[1]) then -- release spy gun from other members and give replacement weapon printl('- %s | releasing gun %s from member %s', task_id, se_obj:section_name(), se_obj.parent_id) local sq_loadout = generate_squad_loadout(task_id) for i=1,#sq_loadout do alife_create(sq_loadout[i], se_obj.position, se_obj.m_level_vertex_id, se_obj.m_game_vertex_id, se_obj.parent_id) end alife_release(se_obj) end end end -- give weapon to spy local se_spy = alife_object(spy_id) for i=1,#spy_loadout do alife_create_item(spy_loadout[i], se_spy) printl('- %s | given [%s] to spy %s', spy_loadout[i], task_id, spy_id) end -- save var var.spy_id = spy_id save_var(db.actor, task_id, var) printl('- %s | returning stage 1', task_id) return 1 end stage_f[1] = function(task, task_id) -- fail if spy dies, make spy friend if last member of squad local var = load_var(db.actor, task_id) if (not var) then printf('var nil in task %s', task_id) return "fail" end local squad_id = var.squad_id local spy_id = var.spy_id local se_squad = alife_object(squad_id) if not se_squad then printf('se_squad nil in task %s', task_id) return "fail" end local se_spy = spy_id and alife_object(spy_id) if not se_spy then printf('se_spy nil in task %s', task_id) return "fail" end if (not se_spy:alive()) then printf('se_spy has died in task %s', task_id) return "fail" end local function only_spy_left() for k in se_squad:squad_members() do if not (k.id == spy_id) then -- printl('other member left %s', k.id) return false end end return true end -- make companion and change to giver faction if only_spy_left() then local npc = level.object_by_id(spy_id) if (not npc) then printl('! %s | stage 1: spy alone offline', task_id) return "fail" end npc:set_relation(game_object.friend,db.actor) npc:set_character_community(var.giver_comm, 0, 0 ) printl('- %s | spy is friend and same faction', task_id) axr_companions.companion_squads[squad_id] = se_squad local se_spy = alife_object(spy_id) local sname = se_spy:name() se_save_var(spy_id,sname,"companion",true) se_save_var(spy_id,sname,"companion_cannot_dismiss",true) se_save_var(spy_id,sname,"companion_cannot_teleport",nil) axr_companions.setup_companion_logic(npc,db.storage[spy_id],false,true) printl('- %s | spy is companion', task_id) join_message(task_id, spy_id) printl('- %s | returning stage 2', task_id) return 2 end end stage_f[2] = function(tsk, task_id) -- if close enough to giver then release companion local var = load_var(db.actor, task_id) if (not var) then printf('var nil in task %s', task_id) return "fail" end local squad_id = var.squad_id local spy_id = var.spy_id local spy_obj = spy_id and level.object_by_id(spy_id) local se_squad = squad_id and alife_object(squad_id) if (not se_squad) then printf('se_squad nil in task %s', task_id) return "fail" end -- For some funny reason, I encoutered a bug where engine can't get the game object, but it works for the server object of task giver local se_giver = alife_object(tsk.task_giver_id) local obj_giver = level.object_by_id(tsk.task_giver_id) local pos_giver = obj_giver and obj_giver:position() or se_giver and se_giver.position local distance = pos_giver and spy_obj and pos_giver:distance_to_sqr(spy_obj:position()) or 9999 if (distance < 100) then -- printl('distance: %s', distance) se_squad.scripted_target = nil se_squad.current_action = nil axr_companions.companion_squads[se_squad.id] = nil xr_logic.restore_scheme_and_logic(spy_obj) spy_obj:disable_info_portion("npcx_is_companion") spy_obj:disable_info_portion("npcx_beh_cannot_dismiss") local se_spy = alife_object(spy_id) if se_spy then local sname = se_spy:name() se_save_var(spy_id,sname,"companion",nil) se_save_var(spy_id,sname,"companion_cannot_dismiss",nil) se_save_var(spy_id,sname,"companion_cannot_teleport",nil) printl('- %s | spy left party', task_id) end leave_message(task_id, spy_id) -- need to remove squad, player_id remains the original so it can get targeted by other tasks SIMBOARD:remove_squad(se_squad) printl('- %s | returning stage 3', task_id) return 3 end if (not spy_obj) then -- spy offline, probably other level, teleport to player TeleportSquad(se_squad,db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id()) -- TODO: check if it causes continuous tp because gets teleported but update after that is still offline end end task_status_functor.spy_task_status_functor = function(tsk,task_id) if not (db.actor and tsk) then return end local stage = tsk.stage local ret = stage_f[stage] and stage_f[stage](tsk,task_id) or stage -- printl('setting task stage to %s', ret) if tonumber(ret) then tsk.stage = ret else return ret end end ---------------------------< Precondition >--------------------------- local function evaluate_squad(targets, id, squad, faction_lookup, is_scripted) if faction_lookup[squad.player_id] then local smart_id = squad.current_target_id local smrt = smart_id and SIMBOARD.smarts[smart_id] local smart = smrt and smrt.smrt local smart_name = smart and smart:name() if (squad:npc_count() > 1) and ((not smart_name) or (smart_name and (simulation_objects.base_smarts[smart_name] ~= true))) and (is_scripted or (not squad:get_script_target())) then targets[#targets + 1] = id printl("squad (%s){%s} is added to targets", id, squad:name()) end end end xr_conditions.validate_spy_task = function(actor, npc, p) if not (p and #p >= 5) then return false end local task_id = p[1] if cache[task_id] and cache[task_id].squad_id and alife_object(cache[task_id].squad_id) then return true end local giver_faction = p[2] local is_enemy = p[3] == "true" and true or false local is_scripted = p[4] == "true" and true or false local scan_mode = p[5] and tonumber(p[5]) or 5 local faction_lookup = {} local actor_level = level.name() if (giver_faction == "nil") then local giver_id = GetEvent("used_npc_id") local se_giver = alife_object(giver_id) giver_faction = se_giver:community() end printl("giver_faction: %s", giver_faction) -- Build list of enemy factions local p_status = parse_list(task_manager.task_ini,task_id,"status_functor_params",true) if (is_enemy) then faction_lookup = p_status else for i=1,#faction_list do local fac = faction_list[i] if game_relations.is_factions_enemies(giver_faction,fac) then faction_lookup[fac] = true printl("Enemy faction: %s", fac) end end end if is_empty(faction_lookup) then printe("! %s | no faction_lookup table", task_id) return false end -- Search for suitable targets local targets = {} local sim = alife() local gg = game_graph() for id,v in pairs(SIMBOARD.squads) do local squad = alife_object(id) if squad then local lvl = sim:level_name(gg:vertex(squad.m_game_vertex_id):level_id()) if (not blacklisted_maps[lvl]) then local is_online = squad.online local is_nearby = sfind(simulation_objects.config:r_value(actor_level, "target_maps", 0, ""), lvl) if ((scan_mode == 1) and is_online) -- same level or ((scan_mode == 2) and (is_online or is_nearby)) -- same + nearby level or ((scan_mode == 3) and is_nearby) -- nearby levels only or ((scan_mode == 4) and (not (is_online or is_nearby))) -- far levels only or (scan_mode == 5) -- anywhere then evaluate_squad(targets, id, squad, faction_lookup, is_scripted) end end end end if #targets > 0 then local squad_id = targets[math.random(#targets)] cache[task_id] = { squad_id = squad_id, giver_comm = giver_faction, } printdbg("- %s | total targets: %s - selected squad: %s", task_id, #targets, squad_id) return true end printdbg("! %s | not targets found", task_id) return false end ---------------------------< Effects >--------------------------- xr_effects.setup_spy_task = function(actor, npc, p) if not (p and p[1]) then return false end local task_id = p[1] if cache[task_id] then local squad_id = cache[task_id].squad_id local squad = squad_id and alife_object(squad_id) if squad then squad.stay_time = game.get_game_time() sim_offline_combat.task_squads[squad_id] = true local tbl = { squad_id = squad_id, giver_comm = cache[task_id].giver_comm, } save_var(db.actor, task_id, tbl) printdbg("- %s | Cached result = squad_id (%s) [%s]", task_id, squad_id, squad.player_id) CreateTimeEvent(0,"setup_spy_task",0,postpone_for_next_frame,task_id, squad_id) end end end